/*
 * Copyright:  2018 smarabbit studio.
 *
 *  Licensed under the Confluent Community License; you may not use this file
 *  except in compliance with the License.  You may obtain a copy of the License at
 *
 *  http://www.confluent.io/confluent-community-license
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 *  WARRANTIES OF ANY KIND, either express or implied.  See the License for the
 *  specific language governing permissions and limitations under the License.
 *
 *  @作   者： 黄开晖 (117227773@qq.com)
 *  @日   期:  2021年8月17日
 */
package com.massyframework.beanskin.maven.plugin;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.security.KeyPair;
import java.security.PublicKey;
import java.util.Collection;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarOutputStream;

import javax.crypto.Cipher;

import org.apache.commons.codec.binary.Hex;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.artifact.handler.ArtifactHandler;
import org.apache.maven.artifact.handler.DefaultArtifactHandler;
import org.apache.maven.execution.MavenSession;
import org.apache.maven.model.Plugin;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugins.annotations.Component;
import org.apache.maven.plugins.annotations.LifecyclePhase;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.project.MavenProject;
import org.apache.maven.project.ProjectBuilder;
import org.codehaus.plexus.util.xml.Xpp3Dom;

import com.massyframework.beanskin.maven.plugin.util.BouncycastleUtils;

/**
 * 复制Mojo
 * @author huangkh
 *
 */
@Mojo(name = "copy", defaultPhase = LifecyclePhase.PROCESS_RESOURCES)
public class CopyMojo extends ArtifactsMojo {

	private static final String ENCRYPTS = "encrypts";
	private static final String CLASS    = "class";
	private static final String SCRIPT   =  "script";

	/**
	 * POM
	 */
	@Parameter(defaultValue = "${project}", readonly = true, required = true)
	private MavenProject project;

	/**
	 * The Maven session
	 */
	@Parameter(defaultValue = "${session}", readonly = true, required = true)
	protected MavenSession session;

	@Component
	private ProjectBuilder projectBuilder;

	/**
	 * Location of the file.
	 */
	@Parameter(defaultValue = "${project.build.directory}", property = "outputDir", required = true)
	private File outputDirectory;

	@Parameter(required = true)
	private Module module;
	@Parameter
	private Certificate certificate;

	private ArtifactHandler artifactHandler = new DefaultArtifactHandler();

	/**
	 * 公钥
	 */
	private PublicKey publicKey;

	/**
	 * 
	 */
	public CopyMojo() {

	}

	@Override
	public void execute() throws MojoExecutionException, MojoFailureException {
		this.checkCertification();
		this.checkModule();
		
		String key = "com.massyframework.beanskin:beanskin-maven-plugin";
				
		Plugin plugin = this.project.getPlugin(key);
		Xpp3Dom configuration = this.getConfiguration(plugin, "copy");

		List<Artifact> artifacts = this.loadArtifactItems(configuration);
		Map<EncryptableArtifact, File> map =  null;
		String pubKeyString = null;
		boolean hasEncrypt = false;
		if (!artifacts.isEmpty()) {
			map = this.getJarFiles(artifacts);
			
			KeyPair keyPair = null;
			Cipher encryptCipher = null;
			Cipher decryptCipher = null;
			
			hasEncrypt =  map.keySet().stream().anyMatch(
					artifact -> artifact.hasEncrpyRequest());
			if (hasEncrypt) {
				try {
					Properties props = BouncycastleUtils.loadProperties();
					PublicKey sharedKey = BouncycastleUtils.loadSharedKey(props);
					
					KeyPair pair = BouncycastleUtils.genericKeyPair();
					PublicKey pubKey = pair.getPublic();
					pubKeyString = BouncycastleUtils.hexToString(pubKey);
					
					byte[] keyData = BouncycastleUtils.calcutaionKey(this.module, pair.getPrivate(), sharedKey);
					encryptCipher = BouncycastleUtils.createEncryptCipher(keyData);
					//decryptCipher = BouncycastleUtils.createDecryptCipher(keyData);
				}catch(MojoExecutionException e) {
					throw e;
				}catch(Exception e) {
					throw new MojoExecutionException(e.getMessage(), e);
				}
			}
			
			if (this.certificate != null) {
				if (encryptCipher == null) {
					try {
						Properties props = BouncycastleUtils.loadProperties();
						PublicKey sharedKey = BouncycastleUtils.loadSharedKey(props);
						
						KeyPair pair = BouncycastleUtils.genericKeyPair();
						PublicKey pubKey = pair.getPublic();
						pubKeyString = BouncycastleUtils.hexToString(pubKey);
					
						byte[] keyData = BouncycastleUtils.calcutaionKey(this.module, pair.getPrivate(), sharedKey);
						encryptCipher = BouncycastleUtils.createEncryptCipher(keyData);
						//decryptCipher = BouncycastleUtils.createDecryptCipher(keyData);
					}catch(MojoExecutionException e) {
						throw e;
					}catch(Exception e) {
						throw new MojoExecutionException(e.getMessage(), e);
					}
				}
				
				try {
					byte[] data = BouncycastleUtils.encrypt(encryptCipher, this.certificate.getMd5());
					String encrypted = Hex.encodeHexString(data);
					this.certificate.setMd5(encrypted);
				} catch (Exception e) {
					throw new MojoExecutionException(e.getMessage(), e);
				} 
			}

			this.copyAndEncrypt(map, keyPair, encryptCipher, decryptCipher);
		}
		
		this.copyAndReplaceModuleConfig(pubKeyString, map.keySet());
	}

	/**
	 * 复制并加密
	 * 
	 * @param map     {@link Map}
	 * @param keyPair {@link KeyPair},密钥对
	 * @param cipher  {@link Cipher},密钥
	 * @throws IOException
	 */
	protected void copyAndEncrypt(Map<EncryptableArtifact, File> map, KeyPair keyPair, 
			Cipher encryptCipher, Cipher decryptCipher) throws MojoExecutionException {
		Path outputDir = this.getLibDirectory(this.module).toPath();

		for (Map.Entry<EncryptableArtifact, File> entry : map.entrySet()) {
			this.copyAndEncrypt(entry.getKey(), entry.getValue(), outputDir, keyPair, encryptCipher, decryptCipher);
		}
	}

	/**
	 * 复制并进行执行类加密
	 * 
	 * @param artifact {@link EncryptableArtifact},支持加密的工件定义
	 * @param jarFile  {@link File}
	 * @throws IOException
	 * @throws MojoExecutionException
	 */
	protected void copyAndEncrypt(EncryptableArtifact artifact, File file, Path outputDir, KeyPair keyPair,
			Cipher encryptCipher, Cipher decryptCipher) throws MojoExecutionException {
		// 先复制jar
		File outputFile = new File(outputDir.toFile(), file.getName());
		this.getLog().info("copying ".concat(file.getPath()).concat(" to ").concat(outputFile.getPath()));
		if (!artifact.hasEncrpyRequest()) {
			try {
				Files.copy(file.toPath(), outputFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
			} catch (Throwable e) {
				throw new MojoExecutionException("copy file failed:" + file.getPath(), e);
			}
		} else {
			// JarEntry
			Set<String> names = new HashSet<String>(artifact.getFileNames());
			artifact.getClassNames().forEach(name -> names.add(name.replace(".", "/").concat(".class")));

			try (JarFile jarFile = new JarFile(file);
				JarOutputStream jos = new JarOutputStream(new FileOutputStream(outputFile))) {
				
				Enumeration<JarEntry> em = jarFile.entries();
				while (em.hasMoreElements()) {
					JarEntry entry = em.nextElement();
					try (InputStream is = jarFile.getInputStream(entry)) {
						if (!names.remove(entry.getName())) {
							try {
								jos.putNextEntry(entry);
								byte[] data = new byte[is.available()];
								is.read(data);
								jos.write(data);
							}catch(Exception e) {
								String message = e.getMessage();
								if (!message.startsWith("duplicate entry")) {
									throw new MojoExecutionException("copy jarEntry failed:" + entry.getName(), e);
								}else {
									this.getLog().warn(message + ", skip.");
								}
							}
						} else {
							try {
								JarEntry newEntry = new JarEntry(entry.getName());
								jos.putNextEntry(newEntry);
								
								byte[] encryped = null;
								if (entry.getName().endsWith(".class")) {
									encryped = BouncycastleUtils.encryptClass(encryptCipher, is);
								}else {
									encryped = BouncycastleUtils.encryptFile(encryptCipher, is);
								}
								
								jos.write(encryped);
								jos.flush();
								this.getLog().info(entry.getName().replace("/", ".") + " encrypted.");
							}catch(Exception e) {
								throw new MojoExecutionException("encrypt jarEntry failed:" + entry.getName(), e);
							}
						}
						jos.closeEntry();
					} catch (MojoExecutionException e) {
						throw e;
					} catch (Exception e) {
						throw new MojoExecutionException("copy or encrypt jarEntry failed:" + entry.getName(), e);
					}					
				}
							
				if (!names.isEmpty()) {
					throw new MojoExecutionException("cannot found class:" + names.iterator().next().replace("/", "."));
				}
			} catch (Exception e) {
				throw new MojoExecutionException(e.getMessage(), e);
			} 
		}
	}

	/**
	 * 复制并且替换模块配置文件
	 * 
	 * @param identifier
	 */
	protected void copyAndReplaceModuleConfig(String pubKeyString, Collection<EncryptableArtifact> artifacts) 
		throws MojoExecutionException{
		File dir = new File(this.project.getBasedir(),
				"src".concat(File.separator).concat("main").concat(File.separator).concat("module"));
		File xmlFile = new File(dir, this.module.getType().concat(".xml"));
		if (!xmlFile.exists()) {
			String message = "cannot found module configuration:".concat(xmlFile.getPath());
			this.getLog().error(message);
			throw new MojoExecutionException(message);
		}

		File outputFile = new File(this.getModuleWorkSpaceDirectory(this.module), xmlFile.getName());
		ModuleXmlWriter writer = new ModuleXmlWriter();
		try {
			this.getLog().info("copying ".concat(xmlFile.getPath()).concat(" to ").concat(outputFile.getPath()));
			writer.copyAndReplace(xmlFile, outputFile, this.module, this.certificate, pubKeyString, artifacts);
		} catch (Exception e) {
			String message = "replace " + xmlFile.getName() + " failed.";
			this.getLog().error(message, e);
			throw new MojoExecutionException(message, e);
		}
	}

	/**
	 * 模块工作空间目录
	 * 
	 * @param identifier {@link Module}
	 * @return {@link File}
	 */
	protected File getModuleWorkSpaceDirectory(Module module) {
		String path = "classes".concat(File.separator).concat("MODULE").concat(File.separator)
				.concat(module.getGroupId()).concat(File.separator).concat(module.getModuleId()).concat(File.separator)
				.concat(module.getVersion());
		File result = new File(this.outputDirectory, path);
		if (!result.exists()) {
			result.mkdirs();
		}
		return result;
	}

	/**
	 * 获取Lib目录
	 * 
	 * @return {@link File}
	 */
	protected File getLibDirectory(Module module) {
		String path = "classes".concat(File.separator).concat("MODULE").concat(File.separator)
				.concat(module.getGroupId()).concat(File.separator).concat(module.getModuleId()).concat(File.separator)
				.concat(module.getVersion()).concat(File.separator).concat("lib");
		File result = new File(this.outputDirectory, path);
		if (!result.exists()) {
			result.mkdirs();
		}
		return result;
	}
	
	/**
	 * 检查证书
	 * <p>
	 * 要不为null,要不有值
	 */
	protected void checkCertification() {
		if (this.certificate == null) return;
		
		if (this.certificate.getName() == null) {
			this.getLog().error("name cannot be set.");
			throw new IllegalArgumentException("certification's name cannot be set.");
		}
		
		if (this.certificate.getMd5() == null) {
			this.getLog().error("md5 cannot be set.");
			throw new IllegalArgumentException("certification's md5 cannot be set.");
		}
	}

	/**
	 * 检查模块信息是否齐全
	 * @return {@link boolean},返回{@code true},表示其全，否则返回{@code false}
	 */
	protected void checkModule(){
		if (this.module.getGroupId() == null) {
			this.getLog().error("groupId cannot be set.");
			throw new IllegalArgumentException("groupId cannot be set.");
		}

		if (this.module.getModuleId() == null) {
			this.getLog().error("moduleId cannot be set.");
			throw new IllegalArgumentException("moduleId cannot be set.");
		}

		if (this.module.getVersion() == null) {
			this.getLog().error("version cannot be set.");
			throw new IllegalArgumentException("version cannot be set.");
		}

		if (this.module.getVersion() == null) {
			this.getLog().error("type cannot be set.");
			throw new IllegalArgumentException("type cannot be set.");
		}
	}

	/**
	 * 获取工件对应的jar文件
	 * 
	 * @param artifacts {@link Artifact}
	 * @return {@link Map}
	 */
	protected Map<EncryptableArtifact, File> getJarFiles(List<Artifact> artifacts) {
		Map<EncryptableArtifact, File> result = new HashMap<>();
		for (Artifact artifact : artifacts) {
			File file = this.locateJarFile(artifact);
			if (file != null) {
				result.put((EncryptableArtifact) artifact, file);
			}
		}
		return result;
	}

	@Override
	protected void customArtifactBuilder(Xpp3Dom item, ArtifactBuilder builder) {
		super.customArtifactBuilder(item, builder);
		EncryptableArtifact.Builder encryptBuilder = (EncryptableArtifact.Builder) builder;
		encryptBuilder.artifactHandler(this.artifactHandler);

		Xpp3Dom encrypts = item.getChild(ENCRYPTS);
		if (encrypts != null) {
			Xpp3Dom[] childs = encrypts.getChildren();
			for (Xpp3Dom child : childs) {
				if (child.getName().equals(CLASS)) {
					String className = child.getValue();
					encryptBuilder.addClassName(className);
					continue;
				}
				if (child.getName().equals(SCRIPT)) {
					String fileName = child.getValue();
					encryptBuilder.addFileName(fileName);
				}
			}
		}
	}

	@Override
	protected ArtifactBuilder createArtifactBuilder() {
		return EncryptableArtifact.newBuilder();
	}

	public PublicKey getPublicKey(Properties props) {
		if (this.publicKey == null) {
			try {
				this.publicKey = BouncycastleUtils.loadPublicKey(props);
			} catch (Exception e) {
				this.getLog().warn("cannot load publicKey.");
			}
		}

		return this.publicKey;
	}

}
